﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using CNStorage;
using CNShapes;
using BobcatUtility;

namespace CNPageViewer
{
    /// <summary>
    /// This control shows all the pages stored in the database. It shows thumbnails
    /// of all the pages in a horizontal strip ordered by the date they were created.
    ///
    /// Double-click to select a page
    /// -----------------------------
    /// If you double-click a page an event will be raised saying that the page was selected.
    /// Note: This is a double-click rather than a single click, as single click is used
    ///       for the drag operation (see below).
    /// 
    /// Alternating colors
    /// ------------------
    /// Pages for different dates are shown with different background colors. All pages
    /// for one date will have the same background color, and ones for the next date will
    /// use a different color. The colors alternate to gove a visual feel for the pages
    /// drawn on different dates.
    /// 
    /// </summary>
    public partial class PageFlowControl : UserControl
    {
        // TODO: There is a bug where if you add a new page to a old session, the page appears in the wrong order in the page-flow control.

        #region Events

        /// <summary>
        /// Args raised with events from this class.
        /// </summary>
        public class Args : EventArgs
        {
            public Guid PageID { get; set; }
        }

        /// <summary>
        /// Event raised when a page is selected.
        /// </summary>
        public event EventHandler<Args> PageSelectedEvent;

        #endregion

        #region Public methods

        /// <summary>
        /// Constructor.
        /// </summary>
        public PageFlowControl()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Sets up the control.
        /// </summary>
        public void initialize()
        {
            // We find the list of all pages from the DB...
            setupPageInfos();

            // We are going to show a scrollbar to control which pages we can see.
            // So we need to work out the 'virtual' width of all the pages, though
            // we will in fact only show them as they scroll into view...
            m_iItemHeight = Height - ctrlScrollbar.Height;
            m_iItemWidth = (int)(m_iItemHeight * ITEM_WIDTH_TO_HEIGHT_RATIO);
            m_iVirtualWidth = m_iItemWidth * m_PageInfos.Count;

            ctrlScrollbar.ValueChanged += new EventHandler(onScroll);
            ctrlScrollbar.LargeChange = Width;

            if (m_iVirtualWidth > Width)
            {
                // There are more pages than we can show, so we set up the scrollbar
                // with the virtual width, and set it to show the most recent pages...
                ctrlScrollbar.Maximum = m_iVirtualWidth;
                ctrlScrollbar.Value = ctrlScrollbar.Maximum - ctrlScrollbar.LargeChange;
            }
            else
            {
                // There are fewer pages that fit in the control, so we don't really
                // need the scrollbar. We will still show it, but set to full width,
                // and showing pages starting from the earliest ones...
                ctrlScrollbar.Maximum = Width;
                ctrlScrollbar.Value = 0;
            }

            // We show the pages...
            onScroll(null, null);

            m_bInitialized = true;
        }

        #endregion

        #region Private functions

        /// <summary>
        /// We need info about every page in the DB. We load and process it here.
        /// </summary>
        private void setupPageInfos()
        {
            m_PageInfos.Clear();

            // We load the pages from the DB. These will be loaded in chronological
            // order...
            IList<Page> pages = DBStorage.getInstance().loadAllPagesWithoutShapes();

            // We want to decide which color to show the page and whether it should have
            // some text. We show each session in alternating colors, and we only show text
            // on the first page in each session...
            int iSession = 0;
            Guid sessionID = new Guid();
            foreach (Page page in pages)
            {
                // We store info for this page in a PageInfo structure...
                PageInfo pageInfo = new PageInfo();
                pageInfo.Page = page;

                // We check if the session ID is he same as the previous
                // page. If it isn't, we've got the first page in a new 
                // session...
                if (page.SessionID.CompareTo(sessionID) != 0)
                {
                    iSession++;
                    sessionID = page.SessionID;
                    pageInfo.Text = page.CreationDateTime.ToString("dd-MMM-yyyy");
                }

                // We alternate colors for each session...
                pageInfo.BackColor = (iSession % 2 == 0) ? SESSION_COLOR_0 : SESSION_COLOR_1;

                m_PageInfos.Add(pageInfo);
            }
        }

        /// <summary>
        /// Called when the scrollbar's value changes. We work out the new set of
        /// items to show.
        /// </summary>
        void onScroll(object sender, EventArgs e)
        {
            // We need to show the set of PageFlowItem controls for the current
            // position of the scrollbar. This is fairly complicated, as the
            // control is working in a 'virtual' mode as there may be lots of 
            // pages in the DB and we do not want to have them all loaded up
            // by this control as it could be slow and use too much memory.

            // So what we do is that we hold a collection of PageFlowItem
            // controls for the current set of items shown. When the scrollbar
            // moves, we update this set. If the scrollbar has only moved a little,
            // the set may already hold the controls to show, and they may just
            // need to be moved a little. If the scrollbar has moved further, 
            // we may need to create new controls to show the items that have 
            // come into view. We will also be able to cleanup the items that 
            // are no longer in view.

            // Once we have the correct set of controls, we decide where they
            // should be shown, and set their positions.

            // We mark the set of page-flow-items currently shown as 'invalid'.
            // As we work out which ones should still be shown, we will mark any
            // that we already hold as valid. This helps us clean up the data later...
            foreach (KeyValuePair<Guid, PageFlowItemControl> pair in m_mapIDToPageFlowItem)
            {
                pair.Value.Valid = false;
            }

            // We find the indexes of the pages to show, based on the position
            // of the scrollbar...
            int iFirstIndex = ctrlScrollbar.Value / m_iItemWidth;
            int iItemsToShow = Width / m_iItemWidth + 2; // We add two to the number of items as there may be items overlapping the edges of the control.

            // We find the pages for these indexes, and update the set of controls
            // we need to show...
            int iFirstControlLeft = -1 * (ctrlScrollbar.Value % m_iItemWidth);
            IDictionary<Guid, PageFlowItemControl> newMap = new Dictionary<Guid, PageFlowItemControl>();
            for (int i = 0; i < iItemsToShow; ++i)
            {
                int iIndex = iFirstIndex + i;
                if (iIndex < 0 || iIndex >= m_PageInfos.Count)
                {
                    continue;
                }

                // We find the page, and check if we already have a control for it...
                PageInfo pageInfo = m_PageInfos[iIndex];
                PageFlowItemControl control;
                Guid pageID = pageInfo.Page.ID;
                if (m_mapIDToPageFlowItem.ContainsKey(pageID) == true)
                {
                    // We've got the control, so we mark it as valid and add it this the
                    // new collection...
                    control = m_mapIDToPageFlowItem[pageID];
                    control.Valid = true;
                }
                else
                {
                    // The control is not in our collection, so we create a new
                    // one...
                    control = createPageFlowItem(pageInfo);
                }

                // We set the control's position...
                control.Left = iFirstControlLeft + i * m_iItemWidth;

                // We add the control to the new collection, i.e. the collection
                // of items now shown...
                newMap[pageID] = control;
            }

            // We can now clean up any unused controls, and replace the collection 
            // of shown controls with the new version...
            cleanupUnusedControls();
            m_mapIDToPageFlowItem = newMap;

            // We make sure that this control does not show its own scrollbar...
            AutoScroll = false;
        }

        /// <summary>
        /// Cleans up and removes any controls that are no longer needed.
        /// </summary>
        private void cleanupUnusedControls()
        {
            foreach (KeyValuePair<Guid, PageFlowItemControl> pair in m_mapIDToPageFlowItem)
            {
                PageFlowItemControl pfic = pair.Value;
                if (pfic.Valid == true)
                {
                    continue;
                }

                // We remove our observation of the click event...
                pfic.MouseClick -= onItemClicked;

                // The page is no longer shown, so we can clear out its shapes...
                pfic.PageViewerControl.Page.clearShapes();

                // And we can remove it from this control...
                pfic.close();
                Controls.Remove(pfic);
            }
        }

        /// <summary>
        /// Creates a new PageFlowItem control to show the page passed in.
        /// </summary>
        private PageFlowItemControl createPageFlowItem(PageInfo pageInfo)
        {
            PageFlowItemControl control = new PageFlowItemControl();

            // We set the control to show this page, and we load the 
            // shapes from the DB...
            control.PageViewerControl.Page = pageInfo.Page;
            DBStorage.getInstance().loadShapesAsync(pageInfo.Page);

            // We set the back color...
            control.BackColor = pageInfo.BackColor;

            // We set the size of the control...
            control.PageViewerControl.AutoZoom = true;
            control.Text = pageInfo.Text;
            control.Height = m_iItemHeight;
            control.Width = m_iItemWidth;
            control.Top = 0;

            // We observe the click event from the control...
            control.MouseClick += onItemClicked;

            // We add it this this control...
            Controls.Add(control);

            return control;
        }

        /// <summary>
        /// Called when one of the page thumbnails is clicked.
        /// </summary>
        void onItemClicked(object sender, MouseEventArgs e)
        {
            PageFlowItemControl pfic = (PageFlowItemControl)sender;
            switch (e.Button)
            {
                case MouseButtons.Left:
                    // When we get a left-click, we raise an event to tell
                    // our observers which page was clicked...
                    Guid pageID = pfic.PageViewerControl.Page.ID;
                    Utility.raiseEvent(PageSelectedEvent, this, new Args { PageID = pageID });
                    break;

                case MouseButtons.Right:
                    // When we get a right-click we show a popup menu...
                    ctrlContextMenu.Show(pfic, e.X, e.Y);
                    break;
            }
        }

        /// <summary>
        /// Called when the control is resized.
        /// </summary>
        private void PageFlowControl_Resize(object sender, EventArgs e)
        {
            // We re-initialize the control. This makes sure that the scrollbar
            // works with the new size...
            reinitialize();
        }

        /// <summary>
        /// Reinitializes the control. This calls initialize, but only if the control
        /// has been initialized before.
        /// </summary>
        private void reinitialize()
        {
            if (m_bInitialized == false)
            {
                return;
            }

            // We unhook from any events that will be hooked up to in initialize, to
            // avoid multiple registration of events...
            ctrlScrollbar.ValueChanged -= new EventHandler(onScroll);
            initialize();
        }

        /// <summary>
        /// Called when the Refresh item is clicked from the right-click
        /// context menu.
        /// </summary>
        private void cmRefresh_Click(object sender, EventArgs e)
        {
            reinitialize();
        }

        #endregion

        #region Private data

        // Consts used for drawing...
        private const double ITEM_WIDTH_TO_HEIGHT_RATIO = 1.5;
        private Color SESSION_COLOR_0 = Color.FromArgb(200, 200, 255);
        private Color SESSION_COLOR_1 = Color.FromArgb(190, 190, 255);

        // The collection of info about each page loaded from the DB...
        private class PageInfo
        {
            public Page Page { get; set; }
            public Color BackColor { get; set; }
            public string Text { get; set; }
        }
        private IList<PageInfo> m_PageInfos = new List<PageInfo>();

        // Flag telling us whether the control has been initialized...
        private bool m_bInitialized = false;

        // The width of the 'virtual' control, i.e. one showing all the pages...
        int m_iVirtualWidth = 1;

        // The size of thumbnail items...
        private int m_iItemWidth = 1;
        private int m_iItemHeight = 1;

        // Collection of PageFlowItem controls currently shown...
        private IDictionary<Guid, PageFlowItemControl> m_mapIDToPageFlowItem = new Dictionary<Guid, PageFlowItemControl>();

        #endregion

    }
}
